package org.beanutopia.reflect;

import com.google.common.base.Nullable;
import com.google.common.base.Predicate;
import org.beanutopia.exception.PropertyDefinitionException;
import org.beanutopia.exception.PropertyManipulationException;
import org.beanutopia.function.Policy;
import org.beanutopia.slot.Slot;

import java.lang.reflect.Method;

/**
 * @author: Yardena
* @date: Oct 27, 2009 5:11:24 PM
*/
class Setter<V> implements SetMethod<V> {
  protected Policy<Object,Method> setters = new Policy<Object,Method>();
  protected String propertyName;
  protected String setterPrefix;
  protected Class<?> objectClass;
  protected Class<V> valueClass;

  Setter(@Nullable Class objectClass, String propertyName, @Nullable Class<V> valueClass,
                   String setterPrefix) {
    this.propertyName = propertyName;
    this.setterPrefix = setterPrefix;
    this.objectClass = objectClass;
    this.valueClass = valueClass;
//    initialize();
  }

  public Setter<V> initialize() throws PropertyDefinitionException {
    if (objectClass != null && valueClass != null) {
      try {
        findMethod(objectClass, valueClass);
      } catch (Exception e) {
        throw new PropertyDefinitionException("Cannot initialize reflector for property " + propertyName, e);
      }
    }
    return this;
  }

  public Policy.Rule<Object,Method> getCachedMethod(Class objectClass) {
    return setters.getRule(objectClass);
  }

  public Method findMethod(Class<?> objectClass, Class<?> valueClass) {
    com.google.common.base.Preconditions.checkNotNull(valueClass, "Value class cannot be null");

    Method setter = null;
    Policy.Rule<Object, Method> rule = getCachedMethod(objectClass);
    if (rule != null) {
      setter = rule.decision();
    } else {
      String methodName = Reflector.buildMethodName(propertyName, setterPrefix).intern();
      for (Method m: objectClass.getMethods()) {
        Class<?>[] p;
        if (methodName.equals(m.getName().intern()) &&
                (p = m.getParameterTypes()).length == 1 &&
                Slot.BOX.apply(p[0]).isAssignableFrom(Slot.BOX.apply(valueClass)) &&
                (setter == null || Slot.BOX.apply(setter.getParameterTypes()[0]).isAssignableFrom(Slot.BOX.apply(p[0])))) {
          setter = m;
        }
      }
      if (setter != null) {
        setters.addRule(org.beanutopia.function.PropertyFunctions.isInstanceOf(setter.getDeclaringClass()), setter);
      } else {
        setters.addRule(org.beanutopia.function.PropertyFunctions.isSupertypeOf(objectClass), null);
      }
    }
    return setter;
  }

  public void apply(Object obj, V val) {
    Method setter = findMethod(obj.getClass(), val.getClass());
    try {
      setter.invoke(obj, val);
    } catch (Exception e) {
      throw new PropertyManipulationException("Cannot reflectively set " + propertyName, e);
    }
  }

  private final Predicate<Object> predicate = new Predicate<Object>() {
    public boolean apply(Object o) {
      if (o == null) return false;
      Class c = o instanceof Class ? (Class)o : o.getClass();
      return findMethod(c, valueClass) != null;
    }
  };

  public Predicate<Object> supported() { return predicate; }

} //end Setter
